package bancosys.tec.persist.builders;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.lang.reflect.TypeVariable;

import jmine.tec.environment.db.DBEnvironmentHolder;
import jmine.tec.persist.PersistMessages;
import jmine.tec.test.Builder;
import jmine.tec.test.TestMessages;
import jmine.tec.utils.reflection.GenericTypeResolverImpl;
import bancosys.tec.component.ControllerLocator;
import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.controller.PersistenceEnabledController;
import bancosys.tec.persist.dao.BaseDAOFactory;
import bancosys.tec.persist.exception.PersistenceException;

/**
 * Classe criada para facilitar a criação de builders para testes de persistência de banco de dados
 * 
 * @param <O> tipo do persistível que o builder constrói
 * @author hugo.posca
 */
public abstract class AbstractDBBuilder<O extends Persistable> implements Builder<O> {

    private static boolean deveGravar = false;

    /**
     * Marca se o builder deve gravar no banco de dados o objeto criado. A atribuição a esta variável estática permite que as dependências
     * de banco de dados sejam satisfeitas ao construirmos objetos com seus respectivos builders
     * 
     * @param deve se deve gravar no banco de dados o objeto criado
     */
    private void deveGravar(boolean deve) {
        deveGravar = deve;
    }

    /**
     * Constrói um objeto persistível com todos os atributos necessários preenchidos
     * 
     * @return Um objeto persistível
     */
    protected abstract O doBuild();

    /**
     * Constrói o persistível associado a este builder, salvando apenas se este método foi chamado através de uma chamada explítica a algum
     * método que obriga a salvar.
     * 
     * @return o persistível associado a este builder
     */
    public final O build() {
        return this.save(this.doBuild());
    }

    /**
     * Constrói e salva o persistível associado a este builder
     * 
     * @return o persistível salvado
     */
    public final O save() {
        this.deveGravar(true);
        try {
            return this.build();
        } finally {
            this.deveGravar(false);
        }
    }

    /**
     * Salva, se necessário, o Persistable passado e o devolve
     * 
     * @param <P> o tipo do Persistable
     * @param persistable o Persistable a ser salvo
     * @return o Persistable, salvo se necessário
     */
    private <P extends Persistable> P save(P persistable) {
        if (deveGravar) {
            persistable.getPurePersister().save();
        }
        return persistable;
    }

    /**
     * @return a DAOFactory
     */
    protected BaseDAOFactory getDAOFactory() {
        return this.getBean(BaseDAOFactory.class, "daoFactory");
    }

    /**
     * Devolve o bean especificado
     * 
     * @param <B> o tipo do bean
     * @param beanClass a classe do bean
     * @param id id do bean
     * @return o bean especificado
     */
    private <B> B getBean(Class<B> beanClass, String id) {
        return beanClass.cast(DBEnvironmentHolder.getInstance().getEnvironment().getHelper().getBeanFactory().getBean(id));
    }

    /**
     * Cria um novo objeto, mapeado no banco de dados, do Persistable passado
     * 
     * @param <P> o tipo do Persistable
     * @param clazz a classe do Persistable
     * @return um novo objeto, mapeado no banco de dados, do Persistable passado
     */
    protected final <P extends Persistable> P createBean(Class<P> clazz) {
        try {
            Constructor constructor = clazz.getDeclaredConstructor((Class[]) null);
            if (!Modifier.isPublic(constructor.getModifiers()) || !Modifier.isPublic(constructor.getDeclaringClass().getModifiers())) {
                constructor.setAccessible(true);
            }

            P bean = clazz.cast(constructor.newInstance(new Object[0]));
            bean.setDirty(true);
            this.injectController(bean);

            return bean;
        } catch (Exception e) {
            throw new PersistenceException(TestMessages.UNABLE_TO_INSTANTIATE_PARAMETER.create(clazz.getName()), e);
        }
    }

    /**
     * @return a new instance of this builder's persistable
     */
    protected final O createBean() {
        return this.createBean(this.getPersistableClass());
    }

    @SuppressWarnings("unchecked")
    private Class<O> getPersistableClass() {
        try {
            TypeVariable typeVariable = AbstractDBBuilder.class.getTypeParameters()[0];
            return (Class<O>) GenericTypeResolverImpl.getInstance().resolveTypeForFixedGenericType(typeVariable, this.getClass());
        } catch (IllegalArgumentException e) {
            throw new PersistenceException(PersistMessages.INVALID_BEAN_IMPLEMENTATION.create(this.getClass()));
        }
    }

    /**
     * Injeta o persiter no bean.
     * 
     * @param bean bean.
     */
    private void injectController(Persistable bean) {
        if (DBEnvironmentHolder.getInstance().getEnvironment() == null) {
            // Sem BeanFactory, Sem Controller.
            return;
        }
        ControllerLocator locator =
                (ControllerLocator) DBEnvironmentHolder.getInstance().getEnvironment().getHelper().getBeanFactory()
                        .getBean("controllerLocator");
        bean.setController((PersistenceEnabledController) locator.findControllerForClass(bean.getClass()));
    }
}
